#include "ToUtf8.h"

#include <io.h>
#include <shlobj_core.h>
#include <shlwapi.h>
#include <tchar.h>
#include <CommCtrl.h>
#include <windowsx.h>

#include <iterator>
#include <filesystem>
#include <fstream>
#include <future>
#include <streambuf>
#include <sstream>
#include <vector>

#include "Resource.h"
#include "StringUtils.h"

namespace fs = std::filesystem;

// 开启系统样式
#if defined _M_IX86
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"")
#elif defined _M_X64
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"")
#else
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
#endif

#pragma comment(lib, "Shlwapi.lib")

/// <summary>取消令牌源</summary>
static Concurrency::cancellation_token_source convert_cts_;
/// <summary>转换任务</summary>
static Concurrency::task<void> convert_task_;

INT_PTR CALLBACK IndicatorWndProc(HWND dlg, UINT msg, WPARAM wp, LPARAM lp)
{
	UNREFERENCED_PARAMETER(lp);
	switch (msg)
	{
	case WM_INITDIALOG:
	{
		OnDialogInit(dlg, *((CvtConf*)lp));
		return (INT_PTR)TRUE;
	}

	case WM_COMMAND:
		if (HIWORD(wp) == EN_CHANGE)
		{
			if (LOWORD(wp) != IDC_SOURCE_FOLDER)
				break;

			int length = GetWindowTextLength(HWND(lp)) + 1; // 终止符
			std::vector<TCHAR> text;
			if (length > 0)
			{
				text.resize(length);
				GetWindowText(HWND(lp), text.data(), length);
			}
			EnableWindow(GetDlgItem(dlg, IDC_SOURCE_CONVERT), PathFileExists(text.data()));
			return (INT_PTR)TRUE;
		}
		if (LOWORD(wp) == IDCANCEL)
		{
			EndDialog(dlg, LOWORD(wp));
			return (INT_PTR)TRUE;
		}
		if (LOWORD(wp) == IDC_SOURCE_BROWSE)
		{
			OnBrowseClicked(dlg);
			return (INT_PTR)TRUE;
		}
		if (LOWORD(wp) == IDC_SOURCE_CONVERT)
		{
			CvtConf config;
			OnConvertClicked(dlg, config);
			return (INT_PTR)TRUE;
		}
		break;
	}
	return (INT_PTR)FALSE;
}

int APIENTRY _tWinMain(_In_ HINSTANCE instance,
	_In_opt_ HINSTANCE prev_instance,
	_In_ LPTSTR cmd_line,
	_In_ int cmd_show)
{
	UNREFERENCED_PARAMETER(prev_instance);
	UNREFERENCED_PARAMETER(cmd_line);

	CvtConf config;
	// .h|.hpp|.hh|.hxx|.c|.cpp|.cc|.cxx
	std::vector<std::wstring> command_args = StringUtils::Split(cmd_line, TEXT("--")); // 空格分隔
	if (!command_args.empty())
	{
		config.command_line = true;

		if (command_args[0] == TEXT("help"))
		{
			std::wstring helper = TEXT(
				R"(--folder=C:\Users	要转换根文件夹名
--extensions=.h|.c	要转换文件扩展名
--excludes=Public	要排除的文件夹名
--codepage=936	要转换文件源编码
--scansub=true	是否搜索子文件夹
--u8bomflag=0	取0强删UTF8标记
取1强加UTF8标记 取2不做任何操作)");
			MessageBox(nullptr, helper.c_str(), TEXT("【命令帮助】"), MB_ICONASTERISK);
			return 0;
		}
	}

	for (auto&& command_arg : command_args)
	{
		std::wstring name;
		std::wstring value;
		std::wistringstream split(command_arg);
		std::getline(split, name, TEXT('\x3D'));
		std::getline(split, value, TEXT('\x3D'));
		auto it = std::remove(value.begin(), value.end(), TEXT('\x22'));
		value.erase(it, value.end()); // 删除 " 双引号
		if (name == TEXT("folder"))
		{
			config.root_folder = value;
		}
		else if (name == TEXT("extensions"))
		{
			config.extensions = StringUtils::Split(value, TEXT('\x7C'));
		}
		else if (name == TEXT("excludes"))
		{
			config.excludes = StringUtils::Split(value, TEXT('\x7C'));
		}
		else if (name == TEXT("codepage"))
		{
			config.code_page = std::wcstoul(value.c_str(), nullptr, 10);
		}
		else if (name == TEXT("scansub"))
		{
			StringUtils::ToBool(value, config.scan_into_sub);
		}
		else if (name == TEXT("u8bomflag"))
		{
			config.with_utf8_bom = BomOperate(_wtoi(value.c_str()));
		}
		else
		{
			MessageBox(nullptr, command_arg.c_str(), TEXT("【无效命令】"), MB_ICONHAND);
			return -1;
		}
	}
	DialogBoxParam(instance, MAKEINTRESOURCE(IDD_MBCSTOUTF8), NULL, IndicatorWndProc, (LPARAM)& config);
	return 0;
}

void OnDialogInit(HWND dlg, const CvtConf& config)
{
	HWND folder = GetDlgItem(dlg, IDC_SOURCE_FOLDER);
	HWND extension = GetDlgItem(dlg, IDC_SOURCE_EXTENSION);
	HWND encoding = GetDlgItem(dlg, IDC_SOURCE_ENCODING);
	HWND excludes = GetDlgItem(dlg, IDC_SOURCE_EXCLUDES);
	HWND subdir = GetDlgItem(dlg, IDC_SCAN_SUBDIR);
	HWND utf8bom = GetDlgItem(dlg, IDC_WITH_UTF8BOM);
	if (config.command_line)
	{
		TCHAR buffer[32]{ 0 };
		_stprintf_s(buffer, TEXT("--- 当前编码(%d) ---"), config.code_page);
		ComboBox_AddString(encoding, buffer);

		SetWindowText(folder, config.root_folder.c_str());

		if (!config.extensions.empty())
		{
			std::wstringstream oss;
			auto& vec = config.extensions;
			if (!vec.empty())
			{
				// Convert all but the last element to avoid a trailing "|"
				std::copy(vec.begin(), vec.end() - 1,
					std::ostream_iterator<std::wstring, wchar_t>(oss, TEXT("\x7C")));

				// Now add the last element with no delimiter
				oss << vec.back();
			}
			SetWindowText(extension, oss.str().c_str());
		}

		if (!config.excludes.empty())
		{
			std::wstringstream oss;
			auto& vec = config.excludes;
			if (!vec.empty())
			{
				// Convert all but the last element to avoid a trailing "|"
				std::copy(vec.begin(), vec.end() - 1,
					std::ostream_iterator<std::wstring, wchar_t>(oss, TEXT("\x7C")));

				// Now add the last element with no delimiter
				oss << vec.back();
			}
			SetWindowText(excludes, oss.str().c_str());
		}

		Button_SetCheck(subdir, config.scan_into_sub);
		Button_SetCheck(utf8bom, config.with_utf8_bom);
	}
	else
	{
		SetWindowText(extension, TEXT(".h|.hpp|.hh|.hxx|.c|.cpp|.cc|.cxx"));

		Button_SetCheck(subdir, BST_CHECKED);
		Button_SetCheck(utf8bom, BST_CHECKED);
	}
	ComboBox_AddString(encoding, TEXT("--- 系统编码(默认) ---"));
	ComboBox_AddString(encoding, TEXT("--- 中文简体(936) ---"));
	ComboBox_AddString(encoding, TEXT("--- 中文繁体(950) ---"));
	ComboBox_SetCurSel(encoding, 0);

	convert_cts_.cancel();
	if (PathFileExists(config.root_folder.c_str()))
	{
		OnConvertClicked(dlg, const_cast<CvtConf&>(config));
	}
}

void OnBrowseClicked(HWND dlg)
{
	HWND folder = GetDlgItem(dlg, IDC_SOURCE_FOLDER);
	int length = GetWindowTextLength(folder) + 1;
	std::vector<TCHAR> text;
	if (length > 0)
	{
		text.resize(length);
		GetWindowText(folder, text.data(), length);
	}

	BROWSEINFO browse_info{};
	browse_info.lpfn = nullptr;
	browse_info.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE;
	browse_info.hwndOwner = dlg;
	browse_info.lpszTitle = TEXT("浏览");
	browse_info.lParam = (LPARAM)text.data();

	LPITEMIDLIST pidl = nullptr;
	if ((pidl = SHBrowseForFolder(&browse_info)) != nullptr)
	{
		wchar_t path[MAX_PATH];
		if (SHGetPathFromIDList(pidl, path))
		{
			SetWindowText(folder, path);
			ILFree(pidl);
		}
	}
}

void OnConvertClicked(HWND dlg, CvtConf config)
{
	EnableWindow(GetDlgItem(dlg, IDC_SOURCE_CONVERT), false);
	if (convert_cts_.get_token().is_canceled())
	{
		convert_cts_ = Concurrency::cancellation_token_source();
	}
	else
	{
		convert_cts_.cancel();
		return;
	}

	HWND convert = GetDlgItem(dlg, IDC_SOURCE_CONVERT);
	HWND progress = GetDlgItem(dlg, IDC_SOURCE_PROGRESS);
	HWND logger = GetDlgItem(dlg, IDC_OUTPUT_LOGGER);

	// 开启跑马灯模式
	LONG_PTR style = GetWindowLongPtr(progress, GWL_STYLE);
	SetWindowLongPtr(progress, GWL_STYLE, style | PBS_MARQUEE);
	SendMessage(progress, (UINT)PBM_SETMARQUEE, (WPARAM)TRUE, (LPARAM)10);

	if (!config.command_line)
	{
		HWND folder = GetDlgItem(dlg, IDC_SOURCE_FOLDER);
		HWND encoding = GetDlgItem(dlg, IDC_SOURCE_ENCODING);
		HWND extension = GetDlgItem(dlg, IDC_SOURCE_EXTENSION);
		HWND exclude = GetDlgItem(dlg, IDC_SOURCE_EXCLUDES);
		HWND subdir = GetDlgItem(dlg, IDC_SCAN_SUBDIR);
		HWND utf8bom = GetDlgItem(dlg, IDC_WITH_UTF8BOM);
		int sel_index = ComboBox_GetCurSel(encoding);
		switch (sel_index)
		{
		case 1:
			config.code_page = 936;
			break;
		case 2:
			config.code_page = 950;
			break;
		default:
			config.code_page = CP_ACP;
			break;
		}

		int length = GetWindowTextLength(folder) + 1;
		std::vector<TCHAR> text;
		if (length > 0)
		{
			text.resize(length);
			GetWindowText(folder, text.data(), length);
		}
		config.root_folder = text.data();
		length = GetWindowTextLength(extension) + 1;
		if (length > 0)
		{
			text.resize(length);
			GetWindowText(extension, text.data(), length);
		}
		config.extensions = StringUtils::Split(text.data(), TEXT('\x7C'));
		length = GetWindowTextLength(exclude) + 1;
		if (length > 0)
		{
			text.resize(length);
			GetWindowText(exclude, text.data(), length);
		}
		config.excludes = StringUtils::Split(text.data(), TEXT('\x7C'));
		config.scan_into_sub = (Button_GetCheck(subdir) == BST_CHECKED);
		config.with_utf8_bom = BomOperate(Button_GetCheck(utf8bom));
	}

	// 相对路径转绝对路径
	for (auto& exclude : config.excludes)
	{
		if (exclude.find(TEXT(":\\")) == -1)
		{
			exclude = config.root_folder + (exclude.front() == TEXT('\\') ? TEXT("") : TEXT("\\")) + exclude;
		}
	}

	convert_task_ = Concurrency::create_task([convert, progress, logger, dlg, config]()
		{
			bool open_dlg = false;
			if (PathFileExists(config.root_folder.c_str()))
			{
				int file_count{ 0 };
				EnableWindow(convert, true);

				Edit_SetText(logger, TEXT(""));
				SetWindowText(convert, TEXT("停止转换(&S)"));
				for (auto& file : GetFiles(config))
				{
					if (convert_cts_.get_token().is_canceled())
					{
						break;
					}
					++file_count;
					auto text = file.substr(config.root_folder.size() + 1);
					SetWindowText(dlg, text.c_str());
				}
				SendMessage(progress, PBM_SETRANGE32, 0, file_count);
				SendMessage(progress, PBM_SETPOS, 0, 0);

				// 关闭跑马灯模式
				LONG_PTR style = GetWindowLongPtr(progress, GWL_STYLE);
				SendMessage(progress, (UINT)PBM_SETMARQUEE, (WPARAM)FALSE, (LPARAM)0);
				SetWindowLongPtr(progress, GWL_STYLE, style & ~PBS_MARQUEE);

				if (!convert_cts_.get_token().is_canceled())
				{
					std::vector<Concurrency::task<bool>> tasks;
					for (auto& file : GetFiles(config))
					{
						tasks.emplace_back(Concurrency::create_task([&, file]()->bool
							{
								auto text = file.substr(config.root_folder.size() + 1);
								SetWindowText(dlg, text.c_str());
								if (!convert_cts_.get_token().is_canceled())
								{
									CvtResult ret = MbcsToUtf8(file, config).get();
									switch (ret)
									{
									case CvtResult::Success:
										Edit_ReplaceSel(logger, (TEXT("转换成功: ") + text + TEXT("\r\n")).c_str());
										break;
									case CvtResult::Invalid:
										Edit_ReplaceSel(logger, (TEXT("无效编码: ") + text + TEXT("\r\n")).c_str());
										break;
									default:
										break;
									}
									SendMessage(progress, PBM_DELTAPOS, 1, 0);
									return true;
								}
								return false;
							}));
					}
					when_all(begin(tasks), end(tasks)).wait();

					open_dlg = (!convert_cts_.get_token().is_canceled());
				}
				SetWindowText(dlg, TEXT("多字节转UTF8"));
			}
			EnableWindow(convert, true);
			SetWindowText(convert, TEXT("开始转换(&C)"));
			convert_cts_.cancel();
			if (open_dlg && MessageBox(dlg, TEXT("是否要打开转换目录？"), TEXT("【转换完成】"), MB_ICONINFORMATION | MB_YESNO) == IDYES)
			{ // 打开转换目录
				OpenFolder(config.root_folder);
			}
		});
}

std::future<CvtResult> MbcsToUtf8(const std::wstring& file, const CvtConf& conf)
{
	fs::path path(file);
	OutputDebugString((file + TEXT("\n")).c_str());

	bool is_found = false;
	for (const auto& extension : conf.extensions)
	{
		if (StringUtils::Equals(extension, path.extension()))
		{
			is_found = true;
			break;
		}
	}

	if (!is_found)
	{ // 跳过非列表内扩展名
		co_return CvtResult::Ignore;
	}

	std::ifstream ifile(path, std::ios::in | std::ios::binary);
	std::string data((std::istreambuf_iterator<char>(ifile)), std::istreambuf_iterator<char>());

	if (data.empty())
	{ // 跳过内容为空的文件
		co_return CvtResult::Ignore;
	}

	std::string utf16be_bom = "\xFE\xFF";
	std::string utf16le_bom = "\xFF\xFE";
	if (data.size() > utf16be_bom.size() && memcmp(data.data(), utf16be_bom.data(), utf16be_bom.size()) == 0)
	{ // 跳过大端 UTF16 编码
		co_return CvtResult::Ignore;
	}
	if (data.size() > utf16le_bom.size() && memcmp(data.data(), utf16le_bom.data(), utf16le_bom.size()) == 0)
	{ // 跳过小端 UTF16 编码
		co_return CvtResult::Ignore;
	}

	int utf8_bom_count = 0;
	std::string utf8_bom = "\xEF\xBB\xBF";
	while (data.size() > utf8_bom.size() && memcmp(data.data(), utf8_bom.data(), utf8_bom.size()) == 0)
	{
		utf8_bom_count++;
		data.erase(data.begin(), data.begin() + utf8_bom.size());
	}

	std::string utf8;
	if (StringUtils::ToUtf8(data, utf8, CP_UTF8))
	{
		if (utf8_bom_count == 1)
		{
			if (conf.with_utf8_bom == BomOperate::Ignore)
			{ // 跳过"忽略"选项
				co_return CvtResult::Ignore;
			}

			if (conf.with_utf8_bom == BomOperate::Append)
			{ // 跳过"追加"选项
				co_return CvtResult::Ignore;
			}
		}
		else if (utf8_bom_count > 1)
		{
			if (conf.with_utf8_bom == BomOperate::Ignore)
			{ // 需要添加BOM
				utf8.insert(utf8.begin(), utf8_bom.begin(), utf8_bom.end());
			}
		}
	}
	else if (!StringUtils::ToUtf8(data, utf8, conf.code_page))
	{ // 非指定编码
		co_return CvtResult::Invalid;
	}

	if (conf.with_utf8_bom == BomOperate::Append)
	{ // 需要添加BOM
		utf8.insert(utf8.begin(), utf8_bom.begin(), utf8_bom.end());
	}

	ifile.close();
	std::ofstream ofile(path, std::ios::out | std::ios::binary | std::ios::trunc);
	std::copy(utf8.begin(), utf8.end(), std::ostreambuf_iterator<char>(ofile));
	ofile.close();
	co_return CvtResult::Success;
}

bool OpenFolder(const std::wstring& folder)
{
	if (SUCCEEDED(CoInitializeEx(nullptr, 0)))
	{
		PIDLIST_ABSOLUTE pidl;
		if (SUCCEEDED(SHParseDisplayName(folder.c_str(), nullptr, &pidl, 0, nullptr)))
		{
			// we don't want to actually select anything in the folder, so we pass an empty
			// PIDL in the array. if you want to select one or more items in the opened
			// folder you'd need to build the PIDL array appropriately
			ITEMIDLIST idl{};
			LPCITEMIDLIST apidl[] = { &idl };
			SHOpenFolderAndSelectItems(pidl, _countof(apidl), apidl, 0);
			ILFree(pidl);
		}
		CoUninitialize();
	}
	return false;
}

std::experimental::generator<std::wstring> GetFiles(const CvtConf& config)
{
	std::vector<std::wstring> folders{ config.root_folder };
	for (size_t i = 0; i < folders.size(); i++)
	{
		auto root = folders[i]; // 这里只能用新的拷贝
		for (auto& file : EnumFiles(root))
		{
			co_yield (file);
		}
		if (config.scan_into_sub)
		{
			for (auto& folder : EnumFolders(root))
			{
				bool add_folder = true;
				for (auto& exclude : config.excludes)
				{
					if (StringUtils::Equals(folder, exclude))
					{ // 只需过滤父文件夹
						add_folder = false;
						break;
					}
				}
				if (add_folder)
				{
					folders.emplace_back(folder);
				}
			}
		}
	}
}

std::experimental::generator<std::wstring> EnumFolders(const std::wstring& folder)
{
	_tfinddata64_t find_data;
	const std::wstring search_pattern = folder + TEXT("\\*");
	const intptr_t find_handle = _tfindfirst64(search_pattern.c_str(), &find_data);

	if (find_handle != -1)
	{
		do
		{
			if (find_data.attrib & _A_SUBDIR)
			{
				if (find_data.name[0] != TEXT('.'))
				{// . 与 .. 首字符均为 .
					co_yield std::move(folder + TEXT("\\") + find_data.name);
				}
			}
		} while (_tfindnext64(find_handle, &find_data) == 0);
		_findclose(find_handle);
	}
}

std::experimental::generator<std::wstring> EnumFiles(const std::wstring& folder)
{
	_tfinddata64_t find_data;
	const std::wstring search_pattern = folder + TEXT("\\*");
	const intptr_t find_handle = _tfindfirst64(search_pattern.c_str(), &find_data);

	if (find_handle != -1)
	{
		do
		{
			if (!(find_data.attrib & _A_SUBDIR))
			{
				co_yield std::move(folder + TEXT("\\") + find_data.name);
			}
		} while (_tfindnext64(find_handle, &find_data) == 0);
		_findclose(find_handle);
	}
}